WebGL 上下文创建与尺寸适配实战
一句话结论:WebGL 画布出问题的两大源头是上下文创建方式与渲染尺寸换算,把 DPR 与视口处理正确,画面模糊和坐标偏移都会消失。
Canvas 上下文创建与尺寸适配
是什么
Canvas 上下文创建与尺寸适配,是指从 canvas 元素取出 WebGL 绘图上下文,并把渲染缓冲的像素尺寸、CSS 显示尺寸、视口范围三者对齐的过程。三者不一致时,画面会模糊、被拉伸或点击坐标偏移。
解决什么问题
它解决三类高频问题:高分屏上画面发虚、窗口缩放后内容变形、鼠标点击位置与绘制位置对不上。本质原因是 canvas 有两套尺寸,渲染缓冲用 width 与 height 属性,页面显示用 CSS 宽高,二者必须按设备像素比换算。
为什么这样做
浏览器 CSS 像素与物理像素不等价。在 DPR 为 2 的手机上,1 个 CSS 像素对应 2 个物理像素,如果缓冲只按 CSS 尺寸创建,浏览器就会把小图拉伸到两倍,边缘必然模糊。反之,如果只放大缓冲却不更新 gl.viewport,绘制内容只会占据画布的四分之一。
怎么用
const canvas = document.getElementById('scene');
const gl = canvas.getContext('webgl', {
alpha: false,
antialias: true,
premultipliedAlpha: false,
powerPreference: 'high-performance',
});
function resize() {
const dpr = Math.min(window.devicePixelRatio || 1, 2); // 上限 2 兼顾性能
const cssW = canvas.clientWidth;
const cssH = canvas.clientHeight;
const bufW = Math.round(cssW * dpr);
const bufH = Math.round(cssH * dpr);
if (canvas.width !== bufW || canvas.height !== bufH) {
canvas.width = bufW;
canvas.height = bufH;
gl.viewport(0, 0, bufW, bufH); // 视口必须与缓冲尺寸一致
}
}
window.addEventListener('resize', resize);
resize();
// 逐帧渲染时先清屏
gl.clearColor(0.06, 0.09, 0.16, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
常见坑
- 只改 CSS 尺寸不改
canvas.width→ 原因:误把 CSS 宽高当渲染尺寸 → 处理:两者分别设置,缓冲尺寸乘 DPR - 改了缓冲尺寸却忘记
gl.viewport→ 原因:视口不会随画布自动更新 → 处理:尺寸变化后立即重设视口 - DPR 直接取原值导致性能骤降 → 原因:三倍屏像素量是两倍屏的两倍多 → 处理:DPR 上限压到 2,或按帧率动态降采样
- 高频 resize 事件里反复重建缓冲 → 原因:未做防抖 → 处理:用
requestAnimationFrame或防抖合并尺寸更新
WebGL 典型应用场景
是什么
WebGL 的典型应用场景,是指需要 GPU 并行绘制能力的网页业务形态,主要分为三维可视化、海量图元渲染与交互式图形应用三类,共同点是数据量或画面复杂度超出 CPU 逐像素绘制的承受范围。
解决什么问题
它解决的是“网页能不能流畅承载复杂图形”的问题。例如十万级数据点的散点图、可自由旋转的产品模型、包含光照与阴影的在线场景,用 DOM 或 Canvas 2D 都会掉到十几帧,而 WebGL 可以稳定在 60 帧。
为什么这样做
这些场景的共同特征是顶点或像素数量巨大、每帧变化规律一致、适合向量化计算。GPU 的并行结构恰好匹配这类负载:相同指令同时作用在成千上万个顶点上,而 CPU 侧只需提交一次数据与状态,CPU 与 GPU 的分工因此最优。
怎么用
| 场景 | 典型形态 | 选型要点 |
|---|---|---|
| 数据可视化 | 大规模散点、流向图、地形热力 | 数据点用 GPU 缓冲或实例化,避免逐点建对象 |
| 3D 展示与配置 | 产品模型、装修与车漆切换 | 用 glTF 资产,减少材质分支 |
| 在线 3D 应用 | 小游戏、模型编辑器 | 关注上下文数量与显存上限,及时释放资源 |
| 数字孪生大屏 | 园区、产线、机房可视化 | 用 LOD 与视锥剔除控制绘制量 |
| 图像与视频处理 | 滤镜、实时特效 | 用离屏帧缓冲做后处理链 |
// 判断当前场景是否值得上 WebGL:顶点或像素量级决定
const isHeavyScene = vertexCount > 50000 || perFramePixels > 2000000;
常见坑
- 把普通图表强行 WebGL 化 → 原因:忽略初始化与显存成本 → 处理:二维简单图表留在 Canvas 2D
- 三维场景不做资源释放 → 原因:页面切换后缓冲与纹理仍占显存 → 处理:销毁视图时调用
deleteBuffer、deleteTexture - 移动端沿用桌面画质 → 原因:移动 GPU 填充率有限 → 处理:按设备能力分级,限制阴影与后处理
要点总结
- canvas 的
width/height是渲染缓冲像素,CSS 宽高是显示尺寸,必须按 DPR 换算 - 尺寸变化后要同步更新
gl.viewport,否则画面只占一角 - DPR 建议上限设为 2,避免高分屏上的填充率浪费
- WebGL 适合三维展示、海量图元与数字孪生,二维简单图表不用
- 视口组件卸载时必须释放缓冲与纹理,防止显存泄漏
